/* eslint-disable no-bitwise */
/**
 * 默认报告样式
 *
 * Created by snowingsea on 2021/01/19.
 */
const fs = require('fs-extra');
const path = require('path');
const compressing = require('compressing');
// eslint-disable-next-line import/no-extraneous-dependencies
const canvas = require('canvas');
const randomString = require('randomstring');
const eCharts = require('echarts');
const moment = require('moment');
const mongoose = require('mongoose');

const Excel = require('./../adapter/excel');
const ProjectVersionSpliter = require('./../../project-version-spliter');
const files = require('./../../../files');
const functions = require('./../../../functions');
const tests = require('./../../../tests');
const utility = require('./../../../utility');
const BaseReporter = require('./../base-report');
const classCreator = require('./../base-report-test-case-transformer');
const { getCountableKinds } = require('../../../file-elements');
const DefaultDataReport = require('./default-data-adapter');
const TargetMachineDataReport = require('./target-machine-data-adapter');
const { defectKindDesc } = require('../../../code-defects');
const { versionType, collectCoverageKind } = require('../../../project-versions');
const logger = require('../../../logger');
const ProgressIndicator = require('../../../progress-indicator');
const codeInstrumentations = require('./../../../code-instrumentation');
const {
  generateStatemetBlocks,
  generateBranchBlocks,
  generateConditionBlocks,
  generateMcDcBlocks,
  generateCondMcDcBlocks,
  generateDecisionBlocks,
} = require('./../functionUtil');
class DefaultReporter extends BaseReporter {
  constructor(
    versionId,
    locale,
    version = null,
    project = null,
    creator = null,
    outputDir = null,
    format, complete,
    reportRange = null,
    includeTestcases = false
  ) {
    super(versionId, locale, version, project, creator, outputDir, __dirname, format);
    this.setDataAdapter(new DefaultDataReport());
    this.complete = complete || false;
    this.jumpable = format === 'html';
    this.reportRange = reportRange;
    this.includeTestcases = includeTestcases;
    this.noFiles = !!(reportRange && reportRange.indexOf(2) < 0);
    this.collectCoverage = null;
    this.errorTests = [];
  }

  setTargetMachineDataSource(data) {
    this.setDataAdapter(new TargetMachineDataReport(data.filesMap, data.functionsMap, data.testcasesMap, this.version, this.project));
  }

  async prepare() {
    if (this.includeTestcases) {
      this.allFuncsOfVersion = new Map();
      this.allFileIdsOfVersion = new Map();
      this.allFilePathOfVersion = new Map();
      const allFiles = await this.getDataAdapter().getFiles(this.versionId);
      allFiles.forEach((file) => {
        const fileId = file._id.toString();
        const obj = {
          _id: file._id,
          fileName: file.fileName,
          path: file.path,
          versionId: file.versionId,
        };
        this.allFileIdsOfVersion.set(fileId, obj);
        this.allFilePathOfVersion.set(file.path, obj);
      });
      const allFuncs = await this.getDataAdapter().getFunctions(this.versionId);
      allFuncs.forEach((func) => {
        this.allFuncsOfVersion.set(func._id.toString(), func);
      });
    }
    if (this.version.codeInsAndResults) {
      const [{ codeInstrumentationId }] = this.version.codeInsAndResults;
      const codeIns = await codeInstrumentations.getCodeInstrumentation(codeInstrumentationId);
      this.collectCoverage = codeIns.coverageStatistics;
    }
  }

  /**
   * 报告生成
   */
  generate(progressIndicator = new ProgressIndicator()) {
    const result = {
      cachePath: this.outputDir,
    };
    const [filesProgressIndicator, summaryProgressIndictor] = progressIndicator.split([0.9, 0.1]);
    return new Promise((resolve, reject) => {
      this.prepare()
        .then(() => this.spliteByFile(null, filesProgressIndicator))
        .then(() => this.generateSummary())
        .then(() => this.generateFailedTests())
        .then(() => {
          summaryProgressIndictor.finish();
          const fileName = `${this.project.projectName}-${this.version.versionName}.${this.getReportName()}${this.getDataAdapter() instanceof TargetMachineDataReport ? `.${this.i18n.__('tests.targetMachine')}` : this.getReportPostfix()}.zip`;
          result.reportPath = path.join(this.outputDir, fileName);
          const reportsDir = path.join(this.outputDir, this.project.projectName);
          return compressing.zip.compressDir(reportsDir, result.reportPath);
        })
        .then(() => {
          resolve(result);
        })
        .catch(reject);
    });
  }

  static handleOverview(overviews, enabledCoverageKind) {
    overviews.forEach((overview) => {
      overview.coverageInfo = [];
      overview.coverageInfoUT = [];
      overview.coverageInfoIT = [];
      enabledCoverageKind.forEach((coverageKind) => {
        let coverage = '-';
        if (overview[coverageKind.totalKey]) {
          coverage = overview[coverageKind.coveredKey]
            / overview[coverageKind.totalKey];
          coverage = `${Math.round(coverage * 10000) / 100}%`;
        }
        const o = {
          coverage,
          total: overview[coverageKind.totalKey],
          covered: overview[coverageKind.coveredKey],
        };
        overview.coverageInfo.push(o);
        if (['statement', 'branch', 'condition', 'mcdc', 'decision'].includes(coverageKind.key)) {
          overview.coverageInfoUT.push(o);
        } else {
          overview.coverageInfoIT.push(o);
        }
      });
    });
    return overviews;
  }

  generateFailedTests() {
    const excel = new Excel(this.creator.username);
    const font = { name: 'Times New Roman', size: 11 };
    const border = {
      top: { style: 'thin' },
      left: { style: 'thin' },
      right: { style: 'thin' },
      bottom: { style: 'thin' },
    };
    excel.addSheet('失败用例');
    excel.setColumn('A', { width: 22 });
    excel.setColumn('B', { width: 22 });
    excel.setColumn('C', { width: 22 });
    excel.setColumn('D', { width: 22 });
    excel.setCell('A1', { value: '文件名', font, border });
    excel.setCell('B1', { value: '函数名', font, border });
    excel.setCell('C1', { value: '用例标识', font, border });
    excel.setCell('D1', { value: '执行结果', font, border });
    this.errorTests.forEach((t, idx) => {
      let result = '';
      switch (t.result) {
        case tests.resultKind.failed:
          result = '未通过';
          break;
        case tests.resultKind.timeout:
        case tests.resultKind.runtimeError:
          result = '运行时错误';
          break;
        case tests.resultKind.compileError:
        case tests.resultKind.instrumentError:
        case tests.resultKind.systemError:
          result = '执行异常';
          break;
        default:
          break;
      }
      excel.setCell(`A${idx + 2}`, { value: t.fileName, font, border });
      excel.setCell(`B${idx + 2}`, { value: t.functionName, font, border });
      excel.setCell(`C${idx + 2}`, { value: t.identifier, font, border });
      excel.setCell(`D${idx + 2}`, { value: result, font, border });
    });
    return excel.writeFile(`${path.join(
      this.outputDir,
      this.project.projectName,
      this.version.versionName,
      'failed-testcases'
    )}.xlsx`);
  }

  /**
   * 生成测试报告总览
   * @param {ProgressIndicator} progressIndicator 进度指示器
   * @returns {Promise}
   */
  generateSummary() {
    if (this.reportRange && this.reportRange.indexOf(1) < 0) {
      return Promise.resolve();
    }
    const summary = {
      reportTime: moment().format('YYYY/MM/DD HH:mm:ss'),
      isIntegrationTest: this.isIntegrationTest(),
      reportName: this.getReportName(),
      coverageRange: !this.version.collectCoverageKind ? collectCoverageKind.all : this.version.collectCoverageKind,
      headerRightContent: `${this.getReportName()} - 总览信息`,
    };
    return this.handleSummary(summary)
      .then(() => Promise.all([
        this.generateCoverageDistributionImage(
          summary.functionsCoverage,
          this.getImageSize().coverageDistributionImage
        ),
        this.generateTestCaseResultsImage(
          summary.testsOverview,
          this.getImageSize().testCaseResultsImage
        ),
        this.generateFunctionResultsImage(
          summary.functionResultsStatistic,
          this.getImageSize().functionResultsImage
        ),
      ]))
      .then(([coverageDistributionImage, testCaseResultsImage, functionResultsImage]) => {
        summary.coverageDistributionImage = coverageDistributionImage.output;
        summary.testCaseResultsImage = testCaseResultsImage.output;
        summary.functionResultsImage = functionResultsImage.output;
        return this.generateSummaryReport(
          summary,
          path.join(
            this.outputDir,
            this.project.projectName,
            this.version.versionName,
            'summary'
          )
        )
          .then(() => {
            fs.remove(coverageDistributionImage.output);
            fs.remove(testCaseResultsImage.output);
            fs.remove(functionResultsImage.output);
          });
      });
  }

  handleSummary(summary) {
    return this.getDataAdapter().getVersionOverview(this.versionId)
      .then((overview) => {
        const enabledCoverageKind = [];
        const enabledUTCoverageKind = [];
        const enabledITCoverageKind = [];
        ['statement', 'branch', 'condition', 'decision', 'mcdc', 'invoke', 'invokedFunc'].forEach((key) => {
          if (this.version.coverages & tests.coverageKind[key]) {
            const o = {
              key,
              coverageKey: `${key}Coverage`,
              totalKey: `${key}Total`,
              coveredKey: `${key}Covered`,
              name: this.i18n.__(`tests.${key}Coverage`),
            };
            enabledCoverageKind.push(o);
            if (['statement', 'branch', 'condition', 'decision', 'mcdc'].includes(key)) {
              enabledUTCoverageKind.push(o);
            } else {
              enabledITCoverageKind.push(o);
            }
          }
          overview[`${key}Coverage`] = '-';
          if (overview[`${key}Total`]) {
            overview[`${key}Coverage`] = overview[`${key}Covered`] / overview[`${key}Total`];
            overview[`${key}Coverage`] = `${Math.round(overview[`${key}Coverage`] * 10000) / 100}%`;
          }
        });
        summary.overview = overview;
        summary.enabledCoverageKind = enabledCoverageKind;
        summary.enabledITCoverageKind = enabledITCoverageKind;
        summary.enabledUTCoverageKind = enabledUTCoverageKind;
        return this.getDataAdapter().getTestResultOverview(this.versionId);
      })
      .then((testResultOverview) => {
        const testsOverview = {
          succeed: 0,
          nonStrictSucceed: 0,
          failed: 0,
          noResult: 0,
          exception: 0,
          codeDefect: 0,
          unknown: 0,
        };
        Object.keys(testResultOverview).forEach((key) => {
          Object.keys(testResultOverview[key]).forEach((testResultKind) => {
            testResultKind = Number(testResultKind);
            const testResultKey = BaseReporter.getTestResultKindKey(testResultKind);
            testsOverview[testResultKey] += testResultOverview[key][testResultKind];
          });
        });
        testsOverview.total = Object.keys(testsOverview)
          .reduce((total, key) => total + testsOverview[key], 0);
        summary.testsOverview = testsOverview;
        return this.getDataAdapter().getFunctionCoverageDistribution(this.versionId);
      })
      .then((functionsCoverage) => {
        summary.functionsCoverage = functionsCoverage;
        summary.functionsCoverage.interval = 25;
        return Promise.resolve();
      })
      .then(() => this.getDataAdapter().getFilesOverview(this.versionId, Object.values(this.version.languages)
        .reduce((arr, item) => arr.concat(item.suffixes), [])))
      .then(({ filesOverview, fileStatusCount }) => {
        summary.sourceFilesOverview = DefaultReporter.handleOverview(
          filesOverview,
          summary.enabledCoverageKind
        );
        summary.sourceFileStatusCount = fileStatusCount.reduce((pre, status) => {
          pre[status.status] = status.count;
          return pre;
        }, {});
        return this.getDataAdapter().getFilesOverview(this.versionId, this.version.headers.suffixes);
      })
      .then(({ filesOverview, fileStatusCount }) => {
        summary.headerFilesOverview = DefaultReporter.handleOverview(
          filesOverview,
          summary.enabledCoverageKind
        );
        summary.headerFileStatusCount = fileStatusCount.reduce((pre, status) => {
          pre[status.status] = status.count;
          return pre;
        }, {});
        return this.getDataAdapter().getFunctionsOverview(this.versionId, { pageSize: 999999 });
      })
      .then(([functionsOverview, funcResultStatistic]) => {
        summary.functionResultsStatistic = funcResultStatistic;
        summary.functionsOverview = DefaultReporter.handleOverview(
          functionsOverview,
          summary.enabledCoverageKind
        );
        if (!this.isIntegrationTest()) {
          return Promise.resolve();
        }
        return utility.arrayChunkOperation(summary.functionsOverview, 20, (func) => this.getDataAdapter().getIntegratedFuncs(func._id, true)
          .then((integratedFuncs) => {
            func.integrationFunctions = integratedFuncs
              .map((relation, index) => ({ name: `${relation.toFuncName}${index === integratedFuncs.length - 1 ? '' : ','}` }));
            func.invokedFuncCoverage = func.invokedFuncTotal ? `${Math.round((func.invokedFuncCovered / func.invokedFuncTotal) * 10000) / 100}%` : '-';
            func.invokeCoverage = func.invokeTotal ? `${Math.round((func.invokeCovered / func.invokeTotal) * 10000) / 100}%` : '-';
          }));
      })
      .then(() => {
        summary.functionsOverview.map((item, index) => {
          item.index = index + 1;
          if (item.resultKind) {
            item.resultKind = this.i18n.__(`tests.functionResult.${item.resultKind}`);
          }
          return item;
        });
      });
  }

  /**
   * 批量处理文件
   * @param {Array} files 文件列表
   * @param {String} versionDir 版本目录
   * @param {Function} callback 回调
   * @param {Number} index 下标
   * @param {ProgressIndicator} progressIndicator 进度指示器
   * @return {Promise}
   */
  handleFiles(files, versionDir, callback = null, progressIndicator) {
    // split reports by files
    if (this.format) {
      return super.handleFiles(files, versionDir, callback, progressIndicator);
    }
    // combined all file report into one.
    const reportPath = `${path.join(versionDir, 'files')}`;
    return Promise.all(
      files.map((file) => super.handleFile(file, versionDir)),
    )
      .then((filesInfo) => {
        progressIndicator.advance(0.5);
        filesInfo = filesInfo.map((fileInfo) => {
          const { typeSystem, functions, resultCount } = fileInfo;
          if (!resultCount) {
            return null;
          }
          return {
            fileName: fileInfo.file.fileName,
            summary: DefaultReporter.getSummaryOfFile(functions),
            functions: this.getFunctionDetailsOfFile(typeSystem, functions),
          };
        });
        filesInfo = filesInfo.filter((item) => item !== null);
        return this.generateFileReport(
          {
            filesInfo,
            isIntegrationTest: this.isIntegrationTest(),
            reportName: this.getReportName(),
            headerRightContent: this.getReportName(),
          },
          reportPath
        )
          .then((res) => {
            progressIndicator.finish();
            return res;
          });
      });
  }

  /**
   * 默认模板显示所有测试用例
   */
  filterTestCasesByResultKind() {
    return false;
  }

  /**
   * 处理单个文件，生成测试用例报告
   * @param {Object}} file 单个文件信息
   * @param {String} versionDir 版本目录
   * @returns {Promise}
   */
  async handleFile(file, versionDir) {
    const reportPath = `${path.join(versionDir, path.dirname(file.path), file.fileName)}`;
    const res = await ProjectVersionSpliter.prototype.handleFile.call(this, file, versionDir);
    const { typeSystem, functions: funcs, resultCount } = res;
    if (!resultCount) {
      return Promise.resolve();
    }
    const fileContent = await files.getLinesOfRemoteFileStream(file._id, {});
    const linesInSourceFile = fileContent.map(({ content }) => content);
    await utility.arrayChunkOperation(funcs, 10, async (func) => {
      func.functionCfg = await this.getDataAdapter().getFunctionCfg(func);
    });
    if (this.willTestStatements()) {
      generateStatemetBlocks(funcs, linesInSourceFile);
    }
    if (this.willTestBranch()) {
      generateBranchBlocks(funcs, linesInSourceFile);
    }
    if (this.willTestCondition()) {
      generateConditionBlocks(funcs);
    }
    if (this.willTestDecision()) {
      generateDecisionBlocks(funcs, linesInSourceFile);
    }
    if (this.willTestMcdc()) {
      // 算法一
      if (this.version.mcdcKind === tests.mcdcKind.masking) {
        generateMcDcBlocks(funcs);
        // 算法二
      } else {
        generateCondMcDcBlocks(funcs);
      }
    }
    funcs.map((func) => {
      if (func.tests.length) {
        func.tests.forEach((test) => {
          if (test.resultKind !== tests.resultKind.unknown
            && test.resultKind !== tests.resultKind.succeed
            && test.resultKind !== tests.resultKind.noResult
            && test.resultKind !== tests.resultKind.nonStrictSucceed) {
            this.errorTests.push({
              fileName: file.fileName,
              functionName: func.functionName,
              identifier: test.identifier,
              result: test.resultKind,
            });
          }
        });
      }
      func.codeCoverageDetails = [];
      if (this.willTestStatements()) {
        func.codeCoverageDetails.push(func.statementsBlocks);
      }
      if (this.willTestBranch() && func.branchTotal > 0) {
        func.codeCoverageDetails.push(func.branchBlocks);
      }
      if (this.willTestCondition() && func.conditionTotal > 0) {
        func.codeCoverageDetails.push(func.conditionsBlocks);
      }
      if (this.willTestDecision() && func.decisionTotal > 0) {
        func.codeCoverageDetails.push(func.decisionBlocks);
      }
      if (this.willTestMcdc() && func.mcdcTotal > 0) {
        func.codeCoverageDetails.push(func.mcdcBlocks || func.condMcdcBlocks);
      }
      return func;
    });
    await this.generateFileReport(
      {
        fileName: file.fileName,
        summary: DefaultReporter.getSummaryOfFile(funcs),
        functions: this.getFunctionDetailsOfFile(typeSystem, funcs),
        isIntegrationTest: this.isIntegrationTest(),
        reportName: this.getReportName(),
        headerRightContent: `${this.getReportName()} - ${file.fileName}`,
      },
      reportPath
    );
    return { reportPath };
  }

  async generateTestcaseFiles(file, funcs, versionDir) {
    const testcasePath = path.join(versionDir, path.dirname(file.path), file.fileName.replace(/\./g, '_'));
    fs.ensureDirSync(testcasePath);
    const funcNameMap = new Map();
    return utility.arrayChunkOperation(funcs, 1, async (func) => {
      if (!func.tests.length) {
        return Promise.resolve();
      }
      if (funcNameMap.has()) {
        funcNameMap.set(func.functionName, funcNameMap.get(func.functionName) + 1);
      } else {
        funcNameMap.set(func.functionName, 0);
      }
      if (this.isIntegrationTest()) {
        const relations = await this.getDataAdapter().retrieveRelations(func._id);
        func.invokeRelations = relations.map((relation) => ({
          fromMangled: relation.fromFuncMangledId,
          fromFilePath: this.allFileIdsOfVersion.get(relation.fromFuncFileId.toString()).path,
          toMangled: relation.toFuncMangledId,
          toFilePath: relation.toFuncFileId && this.allFileIdsOfVersion.has(relation.toFuncFileId.toString()) ? this.allFileIdsOfVersion.get(relation.toFuncFileId.toString()).path : '',
          enableStub: relation.enableStub,
        }));
        tests.replaceFileIdOrFuncIdToPath(
          func.variables,
          this.allFuncsOfVersion,
          this.allFileIdsOfVersion,
          this.allFilePathOfVersion
        );
      } else {
        func.variables = func.variables ? func.variables.toObject({ minimize: false }) : {};
        Object.keys(func.variables).forEach((key) => {
          if (['_id', '__v', 'fileId', 'versionId', 'projectId', 'functionId'].includes(key)) {
            delete func.variables[key];
          }
        });
      }
      // 死循环次数添加mangledId
      if (func.variables.infiniteLoops) {
        func.variables.infiniteLoops.forEach((loop) => {
          if (!this.allFileIdsOfVersion.has(loop.fileId.toString())
            || !this.allFuncsOfVersion.has(loop.functionId.toString())) {
            return;
          }
          loop.mangledId = this.allFuncsOfVersion.get(loop.functionId.toString()).mangledId;
          delete loop.fileId;
          delete loop.functionId;
        });
      }
      const testsData = {
        functionVariables: func.variables,
        functionDesc: func.description,
        functionUdf: func.userDefinedFields,
        invokeRelations: func.invokeRelations,
        testCases: [],
        testCaseGroups: func.testCaseGroups || [],
      };
      func.tests.forEach((test) => {
        if (test.kind !== tests.testKind.modified && test.kind && this.i18n) {
          test.description = this.i18n.__('tests.structuralCoverageCase');
        }
        const newTestCase = {
          _id: test._id,
          resultKind: test.resultKind,
          decisions: test.decisions,
          execCount: test.execCount,
          kind: test.kind,
          data: test.data || {},
          description: test.description,
          requirementInfo: test.requirementInfo,
          number: test.number,
          identifierNumber: test.identifierNumber,
          userDefinedFields: test.userDefinedFields,
          versionType: this.version.versionType,
          typeName: test.typeName,
        };
        if (JSON.stringify(newTestCase.data) === '{}') {
          testsData.testCases.push(newTestCase);
          return;
        }
        if (this.isIntegrationTest()) {
          tests.replaceFileIdOrFuncIdToPath(
            newTestCase,
            this.allFuncsOfVersion,
            this.allFileIdsOfVersion,
            this.allFilePathOfVersion
          );
        }
        testsData.testCases.push(newTestCase);
      });
      return utility.writeFile(JSON.stringify({
        [func.mangledId]: testsData,
      }), path.join(testcasePath, `${func.functionName}${funcNameMap.get(func.functionName) ? `_${funcNameMap.get(func.functionName)}` : ''}.json`));
    });
  }

  /**
   * 获取单个文件概要统计
   * @param {Array} functions 函数列表
   * @returns {Object}
   */
  static getSummaryOfFile(functions) {
    const summary = {
      functionCount: functions.filter(({ kind }) => getCountableKinds().includes(kind)).length,
      tests: {
        total: 0,
        succeed: 0,
        nonStrictSucceed: 0,
        failed: 0,
        noResult: 0,
        exception: 0,
        codeDefect: 0,
        unknown: 0,
      },
    };
    functions.forEach((func) => {
      summary.tests.total += func.tests.length;
      func.tests.forEach((test) => {
        const key = BaseReporter.getTestResultKindKey(test.resultKind);
        test.resultKindName = key;
        if (summary.tests[key] === undefined) {
          return;
        }
        summary.tests[key] += 1;
      });
    });
    return summary;
  }

  /**
   * 获取函数列表详细信息
   * @param {Object} typeSystem 类型系统
   * @param {Array} functions 函数列表
   * @returns {Array}
   */
  getFunctionDetailsOfFile(typeSystem, functions) {
    const resFunctions = [];
    functions.sort((a, b) => {
      if (a.index < b.index) return -1;
      if (a.index > b.index) return 1;
      return 0;
    });
    functions.forEach((func) => {
      if (!func.tests.length) {
        return;
      }
      resFunctions.push({
        functionName: func.functionName,
        description: func.description,
        udfs: func.udfs,
        codeDefects: this.getCodeDefects(func),
        // to be modified
        summary: this.getSummaryOfFunction(func),
        tests: this.getTestDetailsOfFunction(typeSystem, func),
        integrationFunctions: func.integrationFunctions,
        integrationFunctionsCount: func.integrationFunctions.length,
      });
    });
    return resFunctions;
  }

  /**
   * 获取某个函数的概要统计
   * @param {Object}} func 函数信息
   * @returns {Object}
   */
  getSummaryOfFunction(func) {
    let returnType = 'void';
    if (func.variables.output && func.variables.output['%']) {
      returnType = func.variables.output['%']['@type'];
    }
    let { functionNameArgs } = func;
    if (!functionNameArgs) {
      const params = [];
      if (func.variables.variables && func.variables.variables.params) {
        Object.keys(func.variables.variables.params).forEach((paramName) => {
          params.push(`${func.variables.variables.params[paramName]['@type']} ${paramName}`);
        });
      }
      functionNameArgs = `${func.functionName}(${params.join(', ')})`;
    }
    const summary = {
      declaration: `${returnType} ${functionNameArgs};`,
      testCount: func.tests.length,
    };
    const { coverages } = this.version;
    // eslint-disable-next-line no-bitwise
    if (func.statementTotal && (coverages & tests.coverageKind.statement) > 0) {
      summary.statementCoverage = func.statementCovered / func.statementTotal;
      summary.statementCoverage = `${Math.round(summary.statementCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.statement) > 0) {
      summary.statementCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.branchTotal && (coverages & tests.coverageKind.branch) > 0) {
      summary.branchCoverage = func.branchCovered / func.branchTotal;
      summary.branchCoverage = `${Math.round(summary.branchCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.branch) > 0) {
      summary.branchCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.conditionTotal && (coverages & tests.coverageKind.condition) > 0) {
      summary.conditionCoverage = func.conditionCovered / func.conditionTotal;
      summary.conditionCoverage = `${Math.round(summary.conditionCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.condition) > 0) {
      summary.conditionCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.decisionTotal && (coverages & tests.coverageKind.decision) > 0) {
      summary.decisionCoverage = func.decisionCovered / func.decisionTotal;
      summary.decisionCoverage = `${Math.round(summary.decisionCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.decision) > 0) {
      summary.decisionCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.mcdcTotal && (coverages & tests.coverageKind.mcdc) > 0) {
      summary.mcdcCoverage = func.mcdcCovered / func.mcdcTotal;
      summary.mcdcCoverage = `${Math.round(summary.mcdcCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.mcdc) > 0) {
      summary.mcdcCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.invokeTotal && (coverages & tests.coverageKind.invoke) > 0) {
      summary.invokeCoverage = func.invokeCovered / func.invokeTotal;
      summary.invokeCoverage = `${Math.round(summary.invokeCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.invoke) > 0) {
      summary.invokeCoverage = '-';
    }
    // eslint-disable-next-line no-bitwise
    if (func.invokedFuncTotal && (coverages & tests.coverageKind.invokedFunc) > 0) {
      summary.invokedFuncCoverage = func.invokedFuncCovered / func.invokedFuncTotal;
      summary.invokedFuncCoverage = `${Math.round(summary.invokedFuncCoverage * 10000) / 100}%`;
    } else if ((coverages & tests.coverageKind.invokedFunc) > 0) {
      summary.invokedFuncCoverage = '-';
    }
    const coverageArr = [];
    if (summary.invokedFuncCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.invokedFuncCoverageRate'),
        value: summary.invokedFuncCoverage,
      });
    }
    if (summary.invokeCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.invokeCoverageRate'),
        value: summary.invokeCoverage,
      });
    }
    if (summary.statementCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.statementCoverageRate'),
        value: summary.statementCoverage,
      });
    }
    if (summary.branchCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.branchCoverageRate'),
        value: summary.branchCoverage,
      });
    }
    if (summary.conditionCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.conditionCoverageRate'),
        value: summary.conditionCoverage,
      });
    }
    if (summary.decisionCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.decisionCoverageRate'),
        value: summary.decisionCoverage,
      });
    }
    if (summary.mcdcCoverage) {
      coverageArr.push({
        name: this.i18n.__('tests.mcdcCoverageRate'),
        value: summary.mcdcCoverage,
      });
    }
    summary.coverages = coverageArr;
    // summary.codeCoverageDetails = this.sampleReportDetail();
    summary.codeCoverageDetails = func.codeCoverageDetails;
    return summary;
  }

  /**
   * 获取用例列表详细信息
   * @param {Object} typeSystem 类型系统
   * @param {Object} func 函数信息
   */
  getTestDetailsOfFunction(typeSystem, func) {
    const resTests = [];
    func.tests.sort((a, b) => {
      const c1Time = a.group ? mongoose.Types.ObjectId(a.weight.substr(0, a.weight.length - 4)).getTimestamp().getTime()
        : a._id.getTimestamp().getTime();
      const c2Time = b.group ? mongoose.Types.ObjectId(b.weight.substr(0, b.weight.length - 4)).getTimestamp().getTime()
        : b._id.getTimestamp().getTime();
      if (typeof a.group === typeof b.group) {
        return a.weight > b.weight ? 1 : -1;
      }
      return c1Time > c2Time ? 1 : -1;
    });
    const funcVariables = functions.getFunctionVariableJson(func.variables);
    if (!this.isIntegrationTest()) {
      typeSystem = files.getTypesJson(func.fileId, typeSystem);
    }
    func.tests.forEach((test) => {
      const testCase = tests.getJson(test, this.i18n);
      const ReportTestCaseTransformer = classCreator(this.isIntegrationTest() && 'integrationTest');
      const transformer = new ReportTestCaseTransformer(
        testCase,
        funcVariables,
        typeSystem,
        this.i18n
      );
      try {
        transformer.transform(this.complete);
      } catch (error) {
        logger.error(`testCase transform error: fileId: ${func.fileId} function name: ${func.functionName} test case number: ${test.number}:`, error);
        return;
      }

      const { coverages } = this.version;
      let statementCoverage; let branchCoverage; let conditionCoverage; let mcdcCoverage;
      let invokeCoverage; let invokedFuncCoverage; let decisionCoverage;
      if ((coverages & tests.coverageKind.statement) > 0) {
        statementCoverage = '-';
        if (func.statementTotal && !this.isValidResultKind(test.resultKind)) {
          statementCoverage = '0%';
        } else if (func.statementTotal) {
          statementCoverage = test.statementCovered / func.statementTotal;
          statementCoverage = `${Math.round(statementCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.branch) > 0) {
        branchCoverage = '-';
        if (func.branchTotal && !this.isValidResultKind(test.resultKind)) {
          branchCoverage = '0%';
        } else if (func.branchTotal) {
          branchCoverage = test.branchCovered / func.branchTotal;
          branchCoverage = `${Math.round(branchCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.condition) > 0) {
        conditionCoverage = '-';
        if (func.conditionTotal && !this.isValidResultKind(test.resultKind)) {
          conditionCoverage = '0%';
        } else if (func.conditionTotal) {
          conditionCoverage = test.conditionCovered / func.conditionTotal;
          conditionCoverage = `${Math.round(conditionCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.decision) > 0) {
        decisionCoverage = '-';
        if (func.decisionTotal && !this.isValidResultKind(test.resultKind)) {
          decisionCoverage = '0%';
        } else if (func.decisionTotal) {
          decisionCoverage = test.decisionCovered / func.decisionTotal;
          decisionCoverage = `${Math.round(decisionCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.mcdc) > 0) {
        mcdcCoverage = '-';
        if (func.mcdcTotal && !this.isValidResultKind(test.resultKind)) {
          mcdcCoverage = '0%';
        } else if (func.mcdcTotal) {
          mcdcCoverage = test.mcdcCovered / func.mcdcTotal;
          mcdcCoverage = `${Math.round(mcdcCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.invoke) > 0) {
        invokeCoverage = '-';
        if (func.invokeTotal && !this.isValidResultKind(test.resultKind)) {
          invokeCoverage = '0%';
        } else if (func.invokeTotal) {
          invokeCoverage = test.invokeCovered / func.invokeTotal;
          invokeCoverage = `${Math.round(invokeCoverage * 10000) / 100}%`;
        }
      }
      if ((coverages & tests.coverageKind.invokedFunc) > 0) {
        invokedFuncCoverage = '-';
        if (func.invokedFuncTotal && !this.isValidResultKind(test.resultKind)) {
          invokedFuncCoverage = '0%';
        } else if (func.invokedFuncTotal) {
          invokedFuncCoverage = test.invokedFuncCovered / func.invokedFuncTotal;
          invokedFuncCoverage = `${Math.round(invokedFuncCoverage * 10000) / 100}%`;
        }
      }
      if (utility.isArrayType(transformer.input)) {
        transformer.input.forEach((input) => {
          // eslint-disable-next-line no-control-regex
          input.variable = input.variable.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
        });
      }
      let annotations;
      test.udfs.forEach((udf) => {
        if (udf.name === '用例注释') {
          annotations = udf.value;
        }
      });
      resTests.push({
        testId: testCase.testId,
        index: test.number,
        description: testCase.description,
        identifier: testCase.identifier,
        requirementInfo: testCase.requirementInfo,
        typeName: testCase.typeName,
        typeNumber: testCase.typeNumber,
        resultKind: test.resultKindName,
        rKind: test.resultKind,
        error: test.error !== undefined ? test.error
          : (test.resultKind !== tests.resultKind.unknown
          && test.resultKind !== tests.resultKind.succeed
          && test.resultKind !== tests.resultKind.failed
          && test.resultKind !== tests.resultKind.noResult
          && test.resultKind !== tests.resultKind.nonStrictSucceed),
        udfs: test.udfs,
        detail: {
          prelude: transformer.prelude,
          input: transformer.input,
          output: transformer.output,
          result: transformer.result,
        },
        isIntegrationTest: this.isIntegrationTest(),
        group: test.group ? test.group.map((num) => `${this.i18n.__('tests.group')}${num} / `).join('') : '',
        statementCoverage,
        branchCoverage,
        conditionCoverage,
        decisionCoverage,
        mcdcCoverage,
        invokeCoverage,
        invokedFuncCoverage,
        annotations,
        execCount: testCase.execCount,
      });
    });
    return resTests;
  }

  /**
   * 获取图片路径
   * @return {string}
   */
  getImagePath() {
    return path.join(this.outputDir, `${randomString.generate(16)}.png`);
  }

  getCodeDefects(func) {
    const arr = func.codeDefects.map((cd, idx) => {
      let locationStr = '';
      const loc = cd.locations[0];
      if (loc) {
        if (loc.type === 'single') {
          locationStr = `(${loc.begin.line || '-'}; ${loc.begin.column || '-'})`;
        } else if (loc.type === 'range') {
          locationStr = `${this.i18n.__('tests.range')}:(${loc.begin.line || '-'}; ${loc.begin.column || '-'})-(${loc.end.line || '-'}; ${loc.end.column || '-'})`;
        }
      }
      return {
        desc: cd.description || defectKindDesc[cd.type],
        typeStr: cd.manual ? this.i18n.__('tests.manualDefect') : this.i18n.__('tests.autoDefect'),
        index: idx + 1,
        locationStr,
        testCases: cd.testcases.map((tc) => tc.number).join(','),
      };
    });
    return arr;
  }

  /**
   * 获取EChart图表数据
   * @param {Object} options 配置信息
   * @param {Object} canvasSize 画布大小
   * @return {Buffer}
   */
  static getEChartsImage(options, canvasSize) {
    const ratio = 4;
    canvas.registerFont(path
      .join(__dirname, '..', 'fonts', 'SourceHanSansCN-Bold.otf'), { family: 'Arial' });
    const imageCanvas = canvas.createCanvas(ratio * canvasSize.width, ratio * canvasSize.height);
    eCharts.setCanvasCreator(() => imageCanvas);
    const chart = eCharts.init(imageCanvas);
    chart.setOption(options);
    // return imageCanvas.toBuffer();
    const buf = imageCanvas.toBuffer();
    chart.dispose();
    return buf;
  }

  /**
   * 获取所有函数覆盖率统计直方图
   * @param {Object} resultData 函数覆盖率统计
   * @param {Object} size 图片大小
   * @returns {Promise}
   */
  generateCoverageDistributionImage(resultData, size) {
    return new Promise((resolve, reject) => {
      const styles = {
        statement: {
          name: this.i18n.__('tests.statementCoverage'),
          color: '#56abfa',
        },
        branch: {
          name: this.i18n.__('tests.branchCoverage'),
          color: '#54b184',
        },
        decision: {
          name: this.i18n.__('tests.decisionCoverage'),
          color: '#4E84A8',
        },
        condition: {
          name: this.i18n.__('tests.conditionCoverage'),
          color: '#00C7DE',
        },
        mcdc: {
          name: this.i18n.__('tests.mcdcCoverage'),
          color: '#7c88f7',
        },
      };
      const source = [];
      const series = [];
      // 类目
      const category = ['distribution'];
      Object.keys(resultData).forEach((coverageName) => {
        if (styles[coverageName]) {
          category.push(styles[coverageName].name);
          series.push({ type: 'bar', color: styles[coverageName].color });
        }
      });
      source.push(category);
      // 数据
      for (let i = 0; i < Math.floor(100 / resultData.interval) + 1; i += 1) {
        // 区间
        const leftNumber = i * resultData.interval;
        const rightNumber = (i + 1) * resultData.interval;
        const item = [`[${leftNumber}, ${Math.min(rightNumber, 100)}`];
        if (rightNumber > 100) {
          item[0] += ']';
        } else {
          item[0] += ')';
        }
        // 柱状数据
        Object.keys(resultData).forEach((coverageName) => {
          if (styles[coverageName]) {
            item.push(resultData[coverageName].distribution[i]);
          }
        });
        source.push(item);
      }
      const options = {
        grid: {
          top: '10%',
          bottom: '10%',
          left: '10%',
          right: '10%',
        },
        legend: {
          height: '10%',
          textStyle: {
            fontSize: 30,
          },
        },
        tooltip: {},
        dataset: { source },
        xAxis: {
          type: 'category',
          axisLabel: {
            fontSize: 30,
          },
        },
        yAxis: {
          axisLabel: {
            fontSize: 30,
          },
        },
        series,
      };
      const outputPath = this.getImagePath();
      utility.writeFile(DefaultReporter.getEChartsImage(options, size), outputPath)
        .then(() => {
          resolve({ output: outputPath });
        })
        .catch(reject);
    });
  }

  /**
   * 获取测试用例结果图
   * @param {Object} resultData 测试结果数据
   * @param {Object} size 图片大小
   * @returns {Promise}
   */
  generateTestCaseResultsImage(resultData, size) {
    const totalTestsCnt = Object.keys(resultData)
      .reduce((total, key) => total + resultData[key], 0);
    return new Promise((resolve, reject) => {
      const styles = {
        succeed: {
          name: this.i18n.__('tests.testResult.succeed'),
          color: '#94cd8a',
        },
        nonStrictSucceed: {
          name: this.i18n.__('tests.testResult.nonStrictSucceed'),
          color: '#D1DA11',
        },
        failed: {
          name: this.i18n.__('tests.testResult.failed'),
          color: '#fe807f',
        },
        noResult: {
          name: this.i18n.__('tests.testResult.noResult'),
          color: '#ddd',
        },
        codeDefect: {
          name: this.i18n.__('tests.testResult.runtimeError'),
          color: '#ff9000',
        },
        exception: {
          name: this.i18n.__('tests.testResult.runtimeException'),
          color: '#fecd67',
        },
        unknown: {
          name: this.i18n.__('tests.testResult.unknown'),
          color: '#5188F4',
        },
        default: {
          name: '',
          color: '#d1d1d1',
        },
      };
      const serie = {
        name: 'results',
        type: 'pie',
        data: [],
        top: '10%',
        avoidLabelOverlap: true,
        radius: '70%',
        startAngle: 180,
        minShowLabelAngle: 2e-8,
        label: {
          normal: {
            textStyle: {
              fontSize: 30,
            },
          },
        },
        labelLine: {
          normal: {
            length: 20,
            length2: 40,
          },
        },
        itemStyle: {
          normal: {
            color: (params) => styles[params.data.kindName].color,
          },
        },
      };
      resultData = totalTestsCnt ? resultData : { default: 0 };
      Object.keys(resultData).forEach((kindName) => {
        if (!styles[kindName]) {
          return;
        }
        serie.data.push({
          value: resultData[kindName],
          name: styles[kindName].name,
          kindName,
        });
      });
      const options = {
        title: {
          text: this.i18n.__('tests.testResultStatistic'),
          x: 'center',
          top: 0,
          textStyle: {
            fontweight: 'bold',
            fontSize: 30,
          },
        },
        legend: {
          show: false,
          orient: 'vertical',
          x: 'left',
        },
        series: [serie],
      };
      const outputPath = this.getImagePath();
      utility.writeFile(DefaultReporter.getEChartsImage(options, size), outputPath)
        .then(() => {
          resolve({ output: outputPath });
        })
        .catch(reject);
    });
  }

  generateFunctionResultsImage(resultData, size) {
    return new Promise((resolve, reject) => {
      const styles = {
        undefined: {
          name: this.i18n.__('tests.functionResult.undefined'),
          color: '#ddd',
        },
        failed: {
          name: this.i18n.__('tests.functionResult.failed'),
          color: '#fe807f',
        },
        passed: {
          name: this.i18n.__('tests.functionResult.passed'),
          color: '#94cd8a',
        },
        default: {
          name: '',
          color: '#d1d1d1',
        },
      };
      const serie = {
        name: 'results',
        type: 'pie',
        data: [],
        top: '10%',
        avoidLabelOverlap: true,
        radius: '70%',
        startAngle: 180,
        minShowLabelAngle: 2e-8,
        label: {
          normal: {
            textStyle: {
              fontSize: 30,
            },
          },
        },
        labelLine: {
          normal: {
            length: 20,
            length2: 40,
          },
        },
        itemStyle: {
          normal: {
            color: (params) => styles[params.data.kindName].color,
          },
        },
      };
      Object.keys(resultData).forEach((kindName) => {
        if (!styles[kindName] || resultData[kindName] === 0) {
          return;
        }
        serie.data.push({
          value: resultData[kindName],
          name: styles[kindName].name,
          kindName,
        });
      });
      if (serie.data.length === 0) {
        serie.data.push({
          value: 0,
          name: styles.default.name,
          kindName: 'default',
        });
      }
      const options = {
        title: {
          text: this.i18n.__('tests.functionResultStatistic'),
          x: 'center',
          top: 0,
          textStyle: {
            fontweight: 'bold',
            fontSize: 30,
          },
        },
        legend: {
          show: false,
          orient: 'vertical',
          x: 'left',
        },
        series: [serie],
      };
      const outputPath = this.getImagePath();
      utility.writeFile(DefaultReporter.getEChartsImage(options, size), outputPath)
        .then(() => {
          resolve({ output: outputPath });
        })
        .catch(reject);
    });
  }

  async generateFileReport(data, output) {
    data.jumpable = this.jumpable;
    const template = 'file-template';
    const templatenc = 'file-template-nc';
    if (this.collectCoverage === false) {
      await fs.ensureDir(path.dirname(output));
      return this.generateReport(templatenc, data, output);
    }
    if (this.collectCoverage === true) {
      await fs.ensureDir(path.dirname(output));
      return this.generateReport(template, data, output);
    }
    const normalizedPath = path.normalize(output);
    let pathParts = normalizedPath.split(path.sep);
    pathParts.splice(5, 0, 'no-coverage');
    const ncOutput = pathParts.join(path.sep);
    await fs.ensureDir(path.dirname(ncOutput));
    pathParts = normalizedPath.split(path.sep);
    pathParts.splice(5, 0, 'with-coverage');
    const wcOutput = pathParts.join(path.sep);
    await fs.ensureDir(path.dirname(wcOutput));
    return Promise.all([
      this.generateReport(templatenc, data, ncOutput),
      this.generateReport(template, data, wcOutput),
    ]);
  }

  async generateSummaryReport(data, output, baseDir = this.baseDir) {
    data.jumpable = this.jumpable;
    data.noFiles = this.noFiles;
    let template = 'summary-template';
    if (this.isIntegrationTest()) {
      template = `${template}-it`;
    }
    const templatenc = `${template}-nc`;
    if (this.collectCoverage === false) {
      await fs.ensureDir(path.dirname(output));
      return this.generateReport(templatenc, data, output, { isSummary: true, baseDir });
    }
    if (this.collectCoverage === true) {
      await fs.ensureDir(path.dirname(output));
      return this.generateReport(template, data, output, { isSummary: true, baseDir });
    }
    const normalizedPath = path.normalize(output);
    let pathParts = normalizedPath.split(path.sep);
    pathParts.splice(5, 0, 'no-coverage');
    const ncOutput = pathParts.join(path.sep);
    await fs.ensureDir(path.dirname(ncOutput));
    pathParts = normalizedPath.split(path.sep);
    pathParts.splice(5, 0, 'with-coverage');
    const wcOutput = pathParts.join(path.sep);
    await fs.ensureDir(path.dirname(wcOutput));
    const newData = JSON.parse(JSON.stringify(data));
    await this.generateReport(template, data, wcOutput, { isSummary: true, baseDir });
    const config = { baseDir };
    if (this.format === 'pdf') {
      config.isSummary = true;
    } else if (this.format === 'html') {
      Object.keys(data).forEach((key) => {
        if (key.endsWith('Image')) {
          newData[key] = data[key];
        }
      });
    }
    return this.generateReport(templatenc, newData, ncOutput, config);
  }

  getImageSize() {
    return {
      coverageDistributionImage: { height: 160, width: 540 },
      testCaseResultsImage: { height: 140, width: 240 },
      functionResultsImage: { height: 140, width: 240 },
    };
  }

  isValidResultKind(resultKind) {
    return [
      tests.resultKind.succeed,
      tests.resultKind.nonStrictSucceed,
      tests.resultKind.noResult,
    ].indexOf(resultKind) >= 0;
  }

  static versionTypeSupport(vType) {
    return vType === versionType.unitTest || vType === versionType.integrationTest;
  }
}

/**
 * Export model definition object.
 */
module.exports = DefaultReporter;
